fix(agents): keep Windows checkouts and rendered docs platform-independent - #655
Conversation
zzet
left a comment
There was a problem hiding this comment.
Request changes: two Windows-only regressions are not protected by hosted CI.
-
P2 — Exercise the LF checkout guarantee on Windows (
.gitattributes:14)The Windows job runs
go test ./internal/agents, which does not recurse intointernal/agents/opencode; it therefore never runsTestPluginFailsOpen, the regression detector for CRLF-embeddedgortex.js. Removing this attribute would leave hosted checks green while Windows source builds could ship CRLF assets again. Please add the OpenCode package test under a CRLF-prone Windows checkout, or an equivalent explicit attribute/byte assertion. -
P2 — Run the render golden on Windows (
internal/agents/render.go:357)This second replacement only changes behavior on Windows because
filepath.ToSlashis a no-op in the Linux/macOS jobs.TestAgentsRenderGoldenlives incmd/gortex, while the Windows job tests only./internal/agents, so reverting this line would leave hosted checks green. Please rungo test -timeout=5m -count=1 -run "^TestAgentsRenderGolden$" ./cmd/gortexonwindows-latest, or add a direct Windows-binding unit test.
The implementation otherwise looks correct; I found no security or dead-code defects.
…ndent Three defects that only surface on a Windows checkout, found while enumerating the failures for the windows-latest matrix leg (zzet#652). 1. No .gitattributes, so a Windows checkout rewrites every text file to CRLF (Git for Windows installs with core.autocrlf=true, the GitHub windows runner included). Two consequences: - `internal/agents/opencode/plugin/gortex.js` and `internal/agents/pi/extension/index.ts` are go:embed'd and written verbatim into a user's project, so a source build on Windows ships CRLF assets. TestPluginFailsOpen already catches this: it splits pluginSource on "\n}\n", which no longer matches. - `gofmt -l` flags every Go file in the tree, so a formatting gate on the Windows leg fails wholesale and a Windows developer cannot tell real findings from the noise. The committed blobs are already LF — `git add --renormalize .` reports no content change — so this pins the checkout and rewrites nothing. 2. GlobalPointerBody built the @-include with filepath.Join, embedding `@C:\Users\me\.gortex\instructions\active.md` into ~/.claude/CLAUDE.md. That line is document content, not a filesystem call, and every other path in the same file is '/'-spelled. shellSafeHookBinary already normalises for exactly this reason. UpsertMarkedBlock keys idempotency on the markers, not on the path, so an existing install has its block rewritten in place on the next run. 3. normalizeRender scrubbed only the native spelling of HOME, the repo root and the resolved gortex binary, but a rendered manifest carries '/'-spelled paths by design. On Windows the substitution missed, so TestAgentsRenderGolden leaked a machine-specific absolute into the comparison and drifted for any developer with gortex on PATH. Verification (windows/amd64, go1.26.6, -count=1): internal/agents ok (was ok) internal/agents/opencode 1 -> 0 failures cmd/gortex 19 -> 18 failures internal/agents/claudecode 2 -> 2 failures (both pre-existing: TestResolveHookCommand, TestEmitPluginBundle_HookHandlerExecBit) Newly-broken set is empty. Each fix was sabotage-verified on its own: reverting (1) fails TestPluginFailsOpen, reverting (2) drifts the claude-code golden, reverting (3) drifts claude-code and hermes. The two updated assertions built their expected @-include with filepath.Join, which asserts the native mangling on Windows and passes there whether or not the renderer normalises; they now use path.Join / filepath.ToSlash. As in zzet#646, none of this can fail on the linux/macos matrix — filepath.ToSlash is a no-op on POSIX — so the Windows runner is the only place these bind.
Review follow-up. Both findings were right: neither fix was reachable
from hosted CI, so reverting either would have left every check green.
1. `./internal/agents` is one package and does not recurse, so
TestPluginFailsOpen — the only detector for a CRLF-embedded
gortex.js — never ran on Windows. The step now names
./internal/agents/opencode explicitly.
That still leaves the guard dependent on the runner converting line
endings, so TestEmbeddedTextAssetsArePinnedToLF states the contract
directly instead, in two halves. The byte half reads the embedded
assets and rejects a CRLF sequence: it fires on any checkout that
already converted. The attribute half asks git for the eol attribute
and requires "lf": it fires on the removal itself, and is the half
that still binds on a runner configured not to convert, where a byte
assertion would pass whatever .gitattributes said. It covers the pi
extension too, which nothing covered before.
2. TestAgentsRenderGolden lives in cmd/gortex, which the Windows job only
builds. Rather than add a step for one test, the binding assertion now
lives next to the code in internal/agents, which that job already
runs: TestNormalizeRenderScrubsTheSlashSpelledPath feeds the function
a native home and root with a '/'-spelled body — the shape a renderer
actually emits — and requires both to be scrubbed.
Sabotage-verified, each independently, on windows/amd64 go1.26.6:
- revert the ToSlash pass in normalizeRender
-> "the slash-spelled home survived normalizeRender"
-> "the slash-spelled root survived normalizeRender"
-> "a machine-specific absolute is still in the manifest"
- drop `eol=lf` from .gitattributes, leave the files LF on disk
-> eol attribute is "unspecified", want "lf" (byte half silent,
which is exactly the case it exists for)
- drop `eol=lf`, stage it, re-check out the asset
-> CRLF line ending at byte 40, and the attribute half too
The first attempt at that last one was invalid and is worth recording:
removing .gitattributes from the working tree alone changes nothing,
because git falls back to the staged copy, so the asset came back LF and
the assertion passed. The attribute has to be removed from the index for
the sabotage to be real.
internal/agents ok
internal/agents/opencode ok
a789a01 to
fc56f87
Compare
|
Both right, and both for the same reason: neither fix was reachable from hosted CI, so reverting either would have left every check green. Pushed 1 — the OpenCode package never ranConfirmed: But naming it only helps if the runner actually converts line endings, and I would rather not have the guard depend on that. So I took the second option you offered as well — an explicit assertion — and split it in two, because the halves fail in different situations:
It also covers 2 — the golden is in the wrong package to bindConfirmed: Happy to add the Sabotage, each independently — windows/amd64, go1.26.6
One of those was wrong the first time and it is worth recording: removing
|
|
The failing step is Run it where the dynamic build could not — the one that pulls A registry 502 on This PR touches The other eleven checks are green, and |
zzet
left a comment
There was a problem hiding this comment.
Re-review at head fc56f87 still requires changes.
- P2 — the new EOL test breaks nested or copied source trees
repoRoot assumes that git rev-parse --show-toplevel is the Gortex module root. This was reproduced by placing the exact-head source under an enclosing Git repository at /src: the focused test attempted to open /internal/agents/opencode/plugin/gortex.js instead of /src/internal/agents/opencode/plugin/gortex.js and failed. That makes valid copied, vendored, monorepo, and packaging layouts fail go test.
Please locate the module or source root independently. Use Git attributes only after verifying that the discovered Git top-level is the same checkout; otherwise continue the byte assertion from the module root and skip only the attribute half. Add a nested-parent-worktree regression case.
- P2 — executable-path normalization is still not guarded on Windows
normalizeRender now applies slash-aware scrubbing to gortexBinaryPaths(), but TestNormalizeRenderScrubsTheSlashSpelledPath puts only home and root paths in its input. Reverting only the binary-path loop change leaves the hosted Windows package tests green. Linux and macOS cannot distinguish this branch because filepath.ToSlash is a no-op there, while Windows builds but does not run TestAgentsRenderGolden.
Please include a slash-spelled executable path in the direct Windows-run test and require it to normalize to gortex, or run the end-to-end render golden on windows-latest.
The previous review findings are otherwise fixed, all hosted checks are green, and no security or dead-code issue was found. These remaining test-suite regression and coverage gap block merge.
…crub
Review follow-up; both findings reproduced before fixing.
1. The EOL test located the tree with `git rev-parse --show-toplevel` and
assumed that was the module. Reproduced exactly as described: with the
source at <outer-git>/src, the test opened
<outer-git>/internal/agents/opencode/plugin/gortex.js and failed.
Vendored copies, monorepo subdirectories and packaging staging trees
all have that shape, so `go test` broke in every one of them.
The root now comes from the nearest go.mod above the test's working
directory. The byte half always runs from there. The attribute half
runs only once the discovered git work tree is confirmed to be that
same directory — compared with os.SameFile, not string equality, since
git answers with forward slashes on Windows — and otherwise logs why
it stood down rather than asserting about some other repository.
TestModuleRootFromIgnoresAnEnclosingCheckout pins it, with an
enclosing work tree *and* an enclosing go.mod so neither a git lookup
nor a careless upward walk passes by accident. Re-running the original
nested reproduction now passes with:
module root ...\outer2\src is not its own git work tree (<nil>);
asserted the embedded bytes only, skipped the attribute half
2. TestNormalizeRenderScrubsTheSlashSpelledPath only fed home and root,
so reverting the gortexBinaryPaths loop on its own left the Windows
package tests green — the third scrub target was unguarded. The test
now pins gortexBinaryPaths to a known path and requires the
slash-spelled executable to normalize to bare "gortex". Pinning keeps
the assertion about the scrub rather than about whatever gortex is
installed on the machine; no test in this package calls t.Parallel, so
the swap cannot race one.
Sabotage, reverting only that loop:
the slash-spelled executable survived normalizeRender
machine-specific absolute "/tools/bin/gortex" is still in the manifest
internal/agents ok
internal/agents/opencode ok
|
Both reproduced before fixing. Pushed 1 — the EOL test broke nested treesReproduced exactly as you described. Source copied to
The root now comes from the nearest
The identity check is Same nested reproduction after the fix:
2 — the binary-path scrub was unguardedRight: the test fed only The test now pins Sabotage, reverting only that loop to the native
Separately: |
First tranche of the Windows CI work for #652. Scope is deliberately small and touches no runtime behaviour outside the rendered-document path: it removes the failures that block reading the Windows suite at all.
What is wrong
1. The repository has no
.gitattributes.Git for Windows installs with
core.autocrlf=true— the GitHubwindows-latestrunner included — so a Windows checkout rewrites every text file to CRLF. Two things break:internal/agents/opencode/plugin/gortex.jsandinternal/agents/pi/extension/index.tsarego:embed'd and written verbatim into a user's project. A source build on Windows therefore ships CRLF assets.TestPluginFailsOpenalready detects it — it splitspluginSourceon"\n}\n", which stops matching.gofmt -lflags every Go file in the tree. Control on an untouched file:So any formatting or lint gate on the Windows leg would fail wholesale, and a Windows contributor cannot separate real findings from the noise.
The committed blobs are already LF.
git add --renormalize .reports no content change — only the files this PR edits appear — so this pins the checkout and rewrites nothing.*.ps1keeps CRLF;*.png/*.gzare marked binary.2.
GlobalPointerBodybuilds the@-include withfilepath.Join.On Windows that writes
into
~/.claude/CLAUDE.md. That line is document content, not a filesystem call, and every other path in the file is/-spelled.shellSafeHookBinaryalready normalises unconditionally for exactly this reason, with the rationale written in its doc comment.UpsertMarkedBlockkeys idempotency on the start/end markers (instructions.go:203,:216), not on the path string, so an existing install has its block replaced in place on the next run — no duplicate, no orphan.I checked for a consumer that parses the line back: the only production caller is
claudecode/adapter.go:268, and it only writes.3.
normalizeRenderscrubs only the native spelling.gortexBinaryPaths()collects native paths (os.Executable,exec.LookPath), andhome/rootarrive native too — but a rendered manifest carries/-spelled paths by design. On Windows theReplaceAllnever matches, soTestAgentsRenderGoldenleaks a machine-specific absolute into the comparison:and it drifts for any developer who has gortex installed on PATH. This is the same class the file's own
canonicalManifestKeyalready guards against.Verification
windows/amd64, go1.26.6,
-count=1, measured againstorigin/main699f351e:internal/agentsinternal/agents/opencodecmd/gortexinternal/agents/claudecodeTestResolveHookCommand,TestEmitPluginBundle_HookHandlerExecBit)Newly-broken set is empty.
gofmtandgo vetclean on the touched files.Each fix was sabotage-verified separately:
TestPluginFailsOpen:callHook has no closing brace this test can find@$HOME\.gortex\instructions\active.mdTestAgentsRenderGoldennow passes on Windows with the full PATH — i.e. with gortex actually installed — which it did not before.Two assertions changed, and why
TestGlobalPointerBody_ShapeAndSentinelandTestGlobalInstall_FatToSlimReplacementbuilt their expected@-include withfilepath.Join. That asserts the native mangling on Windows, so it passes there whether or not the renderer normalises. They now usepath.Join/filepath.ToSlash. Ininstall_diet_test.goboth spellings are kept deliberately — the@-include assertion takes the slash form, theos.ReadFilenext to it keeps the native one.Limitation, stated up front
As in #646, none of this can fail on the linux/macos matrix:
filepath.ToSlashis a no-op on POSIX, and the CRLF conversion never happens there. Thewindows-latestleg from #652 is the only place these bind. I have not added a selector step — after your note on #652 that narrow selectors can miss the regression they are meant to protect, that seems like the wrong direction.On the rest of #652
I ran the full suite on
origin/main699f351e(windows/amd64, go1.26.6, no-race): 173 failing tests across 28 packages, of which 2 are my machine, not the repo —internal/analysisTestMapGitDiff*andinternal/releasesleave a.git/ai/working_logs/directory behind that a localgitfork creates, andos.RemoveAllthen fails. I will post the full classification on #652 with the buckets and the three questions that need your call before I can fix them.